#!/usr/bin/env python
# @Date    : 2023-03-17
# @Author  : Bright (brt2@qq.com)
# @Link    : https://gitee.com/brt2
# @Version : 0.1.0

import bottle
import bottle as app  # import Bottle; app = Bottle()
# from bottle import jinja2_template as render_template
from bottle import template as render_template_string
from bottle import request, static_file, redirect

def html_escape(string):
    """ 由于bottle源码中，escape函数是: " -> &quot;
        html_escape函数本身没有对\n进行转换
        html_quote的转换为: \n -> &#10;
        这样导致前端脑图UI解析包含\n的json报错 "Uncaught SyntaxError: Unexpected token"
        这里，参考jinja2的处理结果，直接将所有"\"转换为"\\"即可
    """
    from bottle import html_escape as bottle_escape
    return bottle_escape(string).replace('\\', '\\\\')

#####################################################################
from io import BytesIO
import base64
from PIL import Image
import numpy as np

def pimg2array(pimg):
    """ wrapper to: np.asarray(pil_img) """
    return np.asarray(pimg)

def pimg2bytes(pimg):
    _io = BytesIO()
    pimg.save(_io, "WEBP")
    return _io.getvalue()

def bytes2pimg(io_bytes):
    if isinstance(io_bytes, bytes):
        io_bytes = BytesIO(io_bytes)

    pimg = Image.open(io_bytes)
    return pimg

def bytes2base64(bytes_img):
    return base64.b64encode(bytes_img).decode()

base64_frombytes = bytes2base64

def base64_topil(str_img):
    bytes_img = base64.b64decode(str_img)
    return bytes2pimg(bytes_img)

def base64_toweb(str_base64, format_=None):
    """ format_: "webp" or "jpg"... """
    if format_:
        return f"data:image/{format_};base64,{str_base64}"
    else:
        return "data:image;base64," + str_base64

def array2pimg(im_arr, mode=None):
    """ wrapper to: PIL.Image.fromarray(im_arr, mode=None)
        这里设定mode为显式参数，因为无法通过channel完全确定mode：
        * 2dim: "1", "L", "P", "I", "F"
        * 3dim: "RGB", "BGR"
        * 4dim: "RGBA", "CMYK", "YCbCr"
    """
    if mode == "BGR":
        NotImplementedError("尚未支持OpenCV格式的图像")
    elif mode is None:
        if im_arr.ndim < 3:
            mode = "L"
        elif im_arr.shape[2] == 3:
            mode = "RGB"  # 无法区分BGR (OpenCV)
        elif im_arr.shape[2] == 4:
            mode = "RGBA"
        else:
            raise Exception("未知的图像类型")
    return Image.fromarray(im_arr, mode)

def array2bytes(im_arr):
    # Don't im_arr.tobytes()
    pimg = array2pimg(im_arr)
    return pimg2bytes(pimg)

#####################################################################
template_index = """
<!DOCTYPE HTML>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>RemoteInterpreterVision</title>
    <!-- <link rel="stylesheet" type="text/css" href="static/bootstrap.min.css"> -->
    <style type="text/css">
        .clear {clear: both}
        .imglist img {width: 30%; margin: 0 auto;}
        .imgitem {margin-top: 5px}
    </style>
</head>
<body>
    <!-- <label for="path_img">Image Path</label>
    <input type="text" id="path_img" name="ImagePath">
    <br> -->
    <form action="/mpstack/upload" enctype="multipart/form-data" method="post">
        <input type="file" accept="image/*" multiple="multiple" name="uploads" id="btn_sel" />
        Save on SERVER:
        <input type="checkbox" name="save_on_server" />
        <input type="submit" value="上传图片" id="submit" />
    </form>
    <img id="img_preview" style="width: 50%" />

% for batch in dataset:
    <div class="imglist">
        % for image in batch:
        <img class="imgitem" src="{{image}}" alt="你的图片被外星人劫持了～" />
        % end
        <div class="clear"></div>
    </div>
% end
</body>

<!-- <script type="text/javascript" src="static/bootstrap.min.js" /> -->
<script>

function upload_preview() {
    const btn_sel = document.getElementById("btn_sel")
    const div_img = document.querySelector("#img_preview")

    // btn_sel.addEventListener('click', upload_img);
    btn_sel.onchange = e => {
        const files = e.target.files || e.dataTransfer.files
        if(files) {
            const reader = new FileReader()
            reader.readAsDataURL(files[0])
            reader.onload = function(e) {
                div_img.src = e.target.result  // result即为base64编码格式的地址
            }
        }
    }
}

window.onload = () => {
    // upload_preview();
}
</script>
</html>
"""

# @app.get("/static/<filepath>")
# def file_resource(filepath):
#     return static_file(filepath, root="./static")

@app.get('/')
def index():
    return redirect("/mpstack/img")

app_mpstack = {}

@app.get('/mpstack/<subtype>')
def mpstack_view(subtype):
    mpstack = app_mpstack.get(subtype)
    if not mpstack:
        return f"未知的mpstack关键字【{subtype}】"

    dataset = []
    for items in mpstack.values(as_type="bytes"):
        dataset.append([base64_toweb(bytes2base64(bytes_img)) for bytes_img in items])

    # print(">>>", mpstack.values(), [[b[:30] for b in l] for l in dataset])
    return render_template_string(template_index, dataset=dataset)

path_datadir = "../_data/"

@app.post("/mpstack/upload")
def mpstack_upload():
    # for fname, upload_file in request.files.items():  # FormsDict
    #     print("....", fname, upload_file)
    upload_files = request.files.getall('uploads')
    if upload_files is not None:
        save_on_server = request.forms.get("save_on_server")

        out = []
        for _file in upload_files:
            # print(_file.filename, _file.content_type, request.forms.get("save_on_server"))
            if save_on_server:
                _file.save(path_datadir, overwrite=True)

            pimg = bytes2pimg(_file.file)
            out.append(pimg)

        app_mpstack["img"].put_group(out)

    return redirect("/mpstack/img")

def run_server(port, imgstack=None):
    if imgstack:
        app_mpstack["img"] = imgstack

    app.run(host="0.0.0.0", port=port)  # debug=True

#####################################################################
from multiprocessing import Manager

class MultiprocStack:
    def __init__(self):
        self.queue = Manager().list()

    def __getitem__(self, key):
        return self.queue[key]

    def __setitem__(self, key, value):
        self.queue[key] = value

    def qsize(self):
        return len(self.queue)

    def empty(self):
        return self.qsize() == 0

    def put(self, item):
        self.queue.append(item)

    def get(self):
        return self.queue.pop()

    def top(self):
        if not self.empty():
            return self.queue[-1]

    def clear(self):
        for i in range(self.qsize()):
            self.queue.pop()

    def __repr__(self):
        str_repr = ',\n'.join([str(i) for i in self.values()])
        return f"MultiprocStack DATA: [\n{str_repr}]"

    def show(self):
        from pprint import pprint
        pprint(self.values())

    @staticmethod
    def _as_type(d, as_type=None):
        if as_type is None:
            return d
        elif as_type == "pillow":
            if isinstance(d, Image.Image):
                return d
            elif isinstance(d, bytes):
                return bytes2pimg(d)
            elif isinstance(d, np.ndarray):
                return array2pimg(d)
        elif as_type == "bytes":
            if isinstance(d, bytes):
                return d
            elif isinstance(d, Image.Image):
                return pimg2bytes(d)
            elif isinstance(d, np.ndarray):
                return array2bytes(d)

    def values(self, as_type=None):
        return [self._as_type(d, as_type) for d in self.queue]


class MultiprocCascadingStack(MultiprocStack):
    """ 适配视图的[[], [], ...]嵌套结构，支持一组放置多张图像 """
    def __init__(self):
        super().__init__()
        self.group_counter = Manager().list()

    def put_group(self, list_item):
        len_ = len(list_item)
        if len_ == 0:
            return

        for i in list_item:
            super().put(i)
        self.group_counter.append(len_)

    def top_group(self) -> list:
        if self.empty():
            return []

        len_ = self.group_counter[-1]
        return self.queue[-len_:]

    def get_group(self) -> list:
        if self.empty():
            return []

        len_ = self.group_counter.pop()
        return self.queue.pop(len_)

    def put(self, item):
        print(f">>> 自动转换 MultiprocCascadingStack.put() -> MultiprocCascadingStack.put_group()")
        self.put_group([item, ])

    def top(self):
        if self.group_counter[-1] != 1:
            raise Exception("请使用【MultiprocCascadingStack.top_group()】接口获取数据")
        print(f">>> 自动转换 MultiprocCascadingStack.top() -> MultiprocCascadingStack.top_group()")
        return self.top_group()[0]

    def get(self):
        if self.group_counter[-1] != 1:
            raise Exception("请使用【MultiprocCascadingStack.get_group()】接口获取数据")
        print(f">>> 自动转换 MultiprocCascadingStack.get() -> MultiprocCascadingStack.get_group()")
        return self.get_group()[0]

    def values(self, as_type=None):
        out = []
        index = 0
        for num in self.group_counter:
            group = self.queue[index: index+num]
            out.append([self._as_type(d, as_type) for d in group])
            index += num
        return out

#####################################################################
from multiprocessing import Process

class RemoteInterpreterVision:
    """ webapp子进程的生命周期管理"""
    PORT = 2108

    def __init__(self):
        self.start()

    def __del__(self):
        self.stop()

    def start(self):
        # 进程间共享数据
        self.stack = MultiprocCascadingStack()

        # 启动Flask-UI
        self._pid = Process(target=run_server, kwargs={"port": self.PORT, "imgstack": self.stack})
        self._pid.start()

    def stop(self):
        print(">>> RemoteInterpreterVision::即将关闭web子进程")
        # self._pid.terminate()
        self._pid.join()
        # self._pid = None

    def restart(self):
        self.stop()
        self.start()

    def show(self):
        import socket
        from werkzeug.serving import get_interface_ip

        display_hostname = get_interface_ip(socket.AF_INET)
        print(f">>> RemoteInterpreterVision::请在Client端启动浏览器查看【URL】 http://{display_hostname}:{self.PORT}/")


if __name__ == '__main__':
    rrapp = RemoteInterpreterVision()
    rrapp.show()

    rr = rrapp.stack
    print("[+] rpi对象，即为rvi的数据缓存堆栈:", rr.values(as_type=None))

    rrapp.stop()
